import random
import cs144script_util

import os
from functools import reduce
# set resembler capacity

capacity = 100
read_times = 20
pushes_each_read = 5

IR_path = '/home/lsr/project/Computer_Network/Assignment/script/inorder_testcaseIR'
testcase_path = '/home/lsr/project/Computer_Network/Assignment/apps/lab1_test.hh'
# generate a random byte string

rand_bytes_len = 1000
rand_bytes = random.randbytes(rand_bytes_len)

# rand_bytes_hexdump = cs144script_util.hexdump(rand_bytes,start=0)
# print(f'byte string hexdump: { rand_bytes_hexdump }')

# generate some random ranges to use
rand_bytes_rand_ranges = []

current = 0
n = len(rand_bytes)

## split to generate random ranges
while current < n:
    max_step = n - current
    step = random.randint(1, 100 if max_step > 100 else max_step)

    rand_range_dict = \
        {
            "range" : {
                "start" : current,
                "end"   : current + step - 1
            },
            "with_eof" : False,
            "bytes" : rand_bytes[current:current + step ] 
            # since direct generate boolean here doesn't work well here
            # use a extra step to sel a eof instead
        }
    current += step
    rand_bytes_rand_ranges.append(
        rand_range_dict
    )
# for r in rand_bytes_rand_ranges:
#     start = r['range']['start']
#     end   = r['range']['end'  ]
#     with_eof = r['with_eof']
#     bytes_dump = cs144script_util.hexdump(r['bytes'],start=start)

#     print( f"start:{start} end:{end} with_eof:{with_eof}")
#     print( f"hexdump : {bytes_dump} \n")



## randomly select ranges
sel_list = []
candidate_list = []
### Method 1 : Keep first range always selected
sel_list.append(rand_bytes_rand_ranges[0])
candidate_list.extend(rand_bytes_rand_ranges[1:])

### Method 2 : Use entire list as canditates
# candidate_list.extend(rand_bytes_rand_ranges)
# print('sel_list:')
# for s in sel_list:
#     print(s)
# print('candidate_list:')
# for c in candidate_list :
#     print(c)

sel_list.extend( 
    random.sample(
        candidate_list,
        k = random.randint( 2,len(candidate_list) - 1)
    )
)

### sort
sel_list.sort(key= lambda x : x['range']['start'] )

# sel_list = sel_list[0:1]

### select a range with eof
eof_sel = random.randint(0,len(sel_list) - 1)
sel_list[eof_sel]['with_eof'] = True

# print('\n' + 'random ranges:')
for r in sel_list:
    start = r['range']['start']
    end   = r['range']['end']
    with_eof = r['with_eof']
    hexstr = cs144script_util.hexstr(r['bytes'])
    dump   = cs144script_util.hexdump(r['bytes'],start= r['range']['start'])

    print(f'start:{start} end:{end} with_eof : {with_eof}')
    # print(f'bytes_hex_str: {hexstr}')
    print(f'bytes_hexdump: {dump} \n')

# generate operation serial
operations_list = []

## Method 1: in-order and insert read to generate

for r in sel_list:
    start = r['range']['start']
    end   = r['range']['end']
    bs    = r['bytes']
    is_eof= r['with_eof']
    # spilt to generate in-order segment
    
    range_split_points = cs144script_util.gen_split_points(len(bs))
    
    in_order_list = []

    for i in range( 1,len(range_split_points)):
        elem = {}
        elem['idx']  = start + range_split_points[i-1]
        elem['end']  = start + range_split_points[i] - 1
        elem['bytes']= bs[range_split_points[i-1]:range_split_points[i]]
        in_order_list.append(elem)

    # generate complete random
    for inorder in in_order_list:
        in_order_pack = [ inorder ]

        rand_spans = cs144script_util.gen_rand_spans(20,len(bs))
        in_order_pack.extend([
            {
                'idx'  : start + s,
                'end'  : start + e - 1,
                'bytes': bs[s:e]
            }
            for (s,e) in rand_spans
        ])
        
        random.shuffle(in_order_pack)
        operations_list.extend(
            [ 
                {
                    'operation' : 'push',
                    'attr' : {
                        'idx'  : i['idx'],
                        'bytes': i['bytes'],
                        'with_eof' : is_eof if i['end'] == end else False
                    }
                }
                for i in in_order_pack
            ]
            
        )

operations_split_points = cs144script_util.gen_split_points(len(operations_list))

new_operations_list = []

for i in range(1, len(operations_split_points)):
    mixed_operation = operations_list[operations_split_points[i-1] : operations_split_points[i]]
    read_operations = [
        { 
            'operation' : 'read' ,
            'attr' : { 'nums' : random.randint(1,capacity) }
        }
        for i in range(0,random.randint(10,20))
    ]
    mixed_operation.extend(read_operations)
    random.shuffle(mixed_operation)

    new_operations_list.extend(mixed_operation)

operations_list = new_operations_list

## Method 2: completly OoO

# read_byte_nums = [ random.randint(1,capacity) for i in range(0,read_times) ]

# read_operations = [
#     { 'operation' : 'read' , 'attr' : { 'nums' : i } }
#     for i in read_byte_nums 
# ]

# for ro in read_operations:
#     operations_sublist = []
#     operations_sublist.append(ro)

#     for i in range(0,pushes_each_read) :
#         # choose a range to generate a segment
#         rand_range = random.choice( sel_list )
#         # from this range to choose a sub range
#         r_start = rand_range['range']['start']
#         r_end   = rand_range['range']['end']

#         sub_start = random.randint(r_start,r_end)
#         sub_end   = random.randint(sub_start,r_end)

#         is_eofrange = rand_range['with_eof']
#         eof = True if sub_end == r_end and is_eofrange else False

#         push_operation = {
#             'operation': 'push',
#             'attr' : {
#                 'idx'  : sub_start,
#                 'bytes': rand_bytes[sub_start : sub_end + 1],
#                 'with_eof' : eof
#             }
#         }

#         operations_sublist.append(push_operation)
    
#     operations_list.extend(operations_sublist)

# random.shuffle(operations_list)

# from operations generate checkpoint
# slibing_window = [ False for i in range(0,capacity)]


# use slide window to generate reference
sw_idx         = 0 # next to read
sw_nxt_asm_idx = 0 # next to assemble

eof_get = False
eof_idx = 0 # "next to EOF"

# generate IR and generate test case
IR_file = open(IR_path,'w',encoding='utf-8')

testcase_statements = []
testcase_statements.append(
    f'\tauto test = std::make_unique<ReassemblerTestHarness>({capacity});\n'
)

## statement template
### read sematics
st_ByteAssembled = "\ttest->execute(BytesAssembled({}));\n"
st_ByteAvailable = "\ttest->execute(BytesAvailable(std::string(\"{}\",{})));\n"
st_NotAtEof      = "\ttest->execute(NotAtEof{});\n"
st_AtEof         = "\ttest->execute(AtEof{});\n"
### push sematics
st_SubmitSegment = "\ttest->execute(SubmitSegment{{std::string(\"{}\",{}),{}}}.with_eof({}));\n"
    

for o in operations_list:
    # check operation type
    operation = o['operation']
    window_state = [ sw_idx , sw_nxt_asm_idx , eof_idx , eof_get ]

    # read operation
    if operation == 'read':
        expect_read_nums = o['attr']['nums']
        
        expect_idx       = sw_idx + expect_read_nums
        max_reachable_idx= sw_nxt_asm_idx
        
        final_idx = min( expect_idx , max_reachable_idx )
        if eof_get :
            final_idx = min(eof_idx, final_idx )

        sw_idx = final_idx

        new_window_state = [ sw_idx , sw_nxt_asm_idx , eof_idx ,eof_get ]

        changed = reduce (
            lambda x , y : x or y,
            [ window_state[i] != new_window_state[i] for i in range(0,len(window_state)) ]
        )
        # write to src
        IR_file.write((f'read -> {expect_read_nums} {window_state} -> {new_window_state}'
                            f'{'*' if changed else ' '}\n'))

        testcase_statements.append( st_ByteAssembled.format(sw_nxt_asm_idx) )
        testcase_statements.append( st_ByteAvailable.format(
            cs144script_util.hexstr(rand_bytes[ window_state[0] : new_window_state[0]]),
            new_window_state[0] - window_state[0]
        ))
        testcase_statements.append(
            st_AtEof if eof_get and new_window_state[0] == eof_idx
            else st_NotAtEof
        )

    elif operation == 'push' :
        # get attribute
        at_eof = o['attr']['with_eof']
        idx    = o['attr']['idx']
        bs     = o['attr']['bytes']
        if at_eof :
            eof_idx = idx + len(bs)
            eof_get = True

        # check if at the unassemble_pointer
        unreachable_idx = sw_idx + capacity
        if idx <= sw_nxt_asm_idx:
            if idx + len(bs) >= sw_nxt_asm_idx:
                expect_idx = idx + len(bs)
                final_idx = min( expect_idx , unreachable_idx )

                if eof_get :
                    final_idx = min(final_idx , eof_idx)
                
                sw_nxt_asm_idx = final_idx
            else : 
                pass
        # otherwise do nothing
        else :
            pass

        new_window_state = [ sw_idx , sw_nxt_asm_idx , eof_idx ,eof_get ]
        changed = reduce (
            lambda x , y : x or y,
            [ window_state[i] != new_window_state[i] for i in range(0,len(window_state)) ]
        )

        IR_file.write(f'push -> idx: {idx} len: {len(bs)} EOF: {with_eof} '
                       f'{window_state}->{new_window_state} {'*' if changed else ' '}\n')
        
        hexstr = cs144script_util.hexstr(bs)
        testcase_statements.append(
            st_SubmitSegment.format(
                hexstr,len(bs),idx,
                'true' if at_eof else 'false'
            )
        )
    else :
        raise Exception('No such operation')

IR_file.close()
print(f'sw_idx :{sw_idx}')
print(f'sw_assembled :{sw_nxt_asm_idx}')

testcase_file = open(testcase_path,'w',encoding='utf-8')
function_defination = f'''
#include "fsm_stream_reassembler_harness.hh"

#include <string>
#include <iostream>
#include <memory>
using namespace std;

// hex dump of this generated cases :
{ 
    cs144script_util.hexdump(bs=rand_bytes,prefix='// ' ,start=0)
}
int testfunc(void) {{
\ttry {{
\t// test statement here
\t{ ''.join( map(lambda x : '\t' + x ,testcase_statements) ) }
\t}}
\tcatch (const exception &e) {{
\t\tcerr << "Exception: " << e.what() << endl;
\t\treturn EXIT_FAILURE;
\t}}
\tcerr << "PASS!" << endl;
\treturn EXIT_SUCCESS;
}}
'''
testcase_file.write(function_defination)
testcase_file.close()
